Learning Python’s Decorators and Scope by Touch
This post is English translation of the Japanese version. Since I'm not a native English speaker, please let me know if you find sentences which don't make sense. Cheers!
To have a good command of Python's decorators, it is necessary to know what Python's scope is. But when you try to adapt the decorators to your codes, you might realize you don't know how to actually use them if you just understand the concept of the scope.
In this post, we discuss the characteristics of Python's decorators and scope by touch, or without abstract terms. We will take a trial-and-error approach to understand them with a sample code.
var_a
~var_e
: 5 variablesvar_f
~var_h
: 3 parametersouter(var_f)
: The function nestinginner(var_g)
inner(var_g)
: The main functiondecorator(var_h)
: The function wrappinginner(var_g)
# Python 3.7.3 var_a = "var_a" print('--- L3: Define decorator()') def decorator(var_h): var_c = "var_c" print('--- L6: In decorator() -> VAR_X = {}'.format(VAR_X)) def _decorator(f): var_d = "var_d" print('--- L10: In _decorator() -> VAR_X = {}'.format(VAR_X)) def wrapper(var_g): print("--- L13: Before decorate") print('--- L14: In wrapper() -> VAR_X = {}'.format(VAR_X)) var_e = "var_e" f(var_g) print("--- L17: After decorate") return wrapper return _decorator print('--- L22: Define outer()') def outer(var_f): print("--- L24: Into outer()") var_b = "var_b" print('--- L26: In outer() -> VAR_X = {}'.format(VAR_X)) @decorator("var_h") def inner(var_g): # Examine which variables we can refer or update within the function "inner" print("--- L31: Into inner()") print('[Ref] L32: inner() -> VAR_X = {}'.format(VAR_X)) VAR_X = "CHANGED" print('[Chg] L34: inner() -> VAR_X = {}'.format(VAR_X)) print('--- L36: Execute inner()') inner("var_g") print('--- L38: In outer() -> VAR_X = {}'.format(VAR_X)) print('--- L41: Execute outer()') outer("var_f") print('--- Finish')
We are going to replace the variable VAR_X
by variables from var_a
to var_h
and modify code to refer or update to the variable from inner()
. The goal is to know which variables can be referred to from each scope. In the nested function inner()
, one of these variables is referred, updated and referred again. This sample code is also available on my GitHub. You can also try it yourself.
GitHub - TakumiHaruta/decorator_practice: Easy examples for decorators and scopes
Let's get started with var_a
.
Agenda
- var_a
- var_b
- var_c & var_d & var_e
- var_f
- var_g
- var_h
- Appendix: Examining a complex decorator
- Summary
- References
var_a
var_a
is a global variable defined at the top of the code. Once you replace VAR_X
by var_a
and execute it, you will get the following error.
# Error --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() Traceback (most recent call last): File "a_deco.py", line 42, in <module> outer("var_f") File "a_deco.py", line 37, in outer inner("var_g") File "a_deco.py", line 16, in wrapper f(var_g) File "a_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_a = {}'.format(var_a)) UnboundLocalError: local variable 'var_a' referenced before assignment
You can see the order of execution in the log above. The function defined first was decorator()
at line 3, and outer()
at line 22 followed. After outer()
at line 41 was executed, inner()
was defined with executing decorator()
. It seems like that the global variable var_a
can be referred to from outer()
, decorator()
and _decorator()
. Then the execution went into inner()
at line 36 with executing the inside of wrapper()
. Lastly, UnboundLocalError
occurred when var_a
was referred in inner()
.
UnboundLocalError
means that two var_a
, the local one and the global one have the same name but these are different. The print function at line 32 in inner()
tries to refer a local var_a
first. If it does not exist, the print function is going to search the outer scope of inner()
next. You might think "Call the global var_a
at line 32" and then "inserted CHANGED
into the local var_a
at line 34", but this would not work as a human thinks.
There are three ways to refer or update var_a
from inner()
.
#1. Refer var_a
as the global variable
-> Comment out var_a = "CHANGED"
at line 33.
By not defining the local var_a
in inner()
, all var_a
used in inner()
are going to search outer scope.
# Result 1 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = var_a --- L17: After decorate --- L38: In outer() -> var_a = var_a --- Finish
#2. Refer and update var_a
as the local variable
-> Replace the argument of def inner(var_g)
at line 29 and the parameter of inner(var_g)
at
line 37 to var_a
.
By passing the global var_a
to the parameter of inner()
, you can refer the global var_a
inside inner()
. The value of var_a
was updated to CHANGED
in inner()
, but it was not changed to CHANGED
outside of inner()
after the execution of inner
had been done. The var_a
which was assigned the value CHANGED
is the local variable of inner()
, so the global var_a
was not affected by this assignment.
# Result 2 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = CHANGED --- L17: After decorate --- L38: In outer() -> var_a = var_a --- Finish
#3. Refer and update var_a
as the global variable
-> Use global keyword before referring var_a
at line 32; global var_a
By defining var_a
as the global keyword inside the inner()
function, you can refer and update the global var_a
from inner()
.
# Result 3 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_a = var_a --- L6: In decorator() -> var_a = var_a --- L10: In _decorator() -> var_a = var_a --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_a = var_a --- L31: Into inner() [Ref] L32: inner() -> var_a = var_a [Chg] L34: inner() -> var_a = CHANGED --- L17: After decorate --- L38: In outer() -> var_a = CHANGED --- Finish
var_b
Next, we are going to deal with var_b
which is the local variable inside the outer()
function.
## Error 1 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b Traceback (most recent call last): File "b_deco.py", line 42, in <module> outer("var_h") File "b_deco.py", line 28, in outer @decorator("var_g") File "b_deco.py", line 6, in decorator print('--- L6: In decorator() -> var_b = {}'.format(var_b)) NameError: name 'var_b' is not defined
Unlike var_a
, var_b
can only be referred inside outer()
and functions in decorator()
cannot reach it out. There are several ways to refer var_b
from decorator()
, but this time we are going to just comment out the print functions in decorator()
.
## Error 2 --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() Traceback (most recent call last): File "b_deco.py", line 42, in <module> outer("var_f") File "b_deco.py", line 37, in outer inner("var_g") File "b_deco.py", line 16, in wrapper f(var_g) File "b_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_b = {}'.format(var_b)) UnboundLocalError: local variable 'var_b' referenced before assignment
After going into inner()
, UnboundLocalError
occurred at line 32. The ways of referring to var_b
are exactly the same as #1 and #2 of var_a
, i.e., comment out var_b = "CHANGED"
at line 33 or pass var_a
to the parameter of inner()
. The way of referring to and updating var_b
like #3 is to use nonlocal keyword instead of global keyword.
The result of adding nonlocal var_b
was the following. The change of var_b
in inner()
was reflected on the one in outer()
at line 38.
## Result --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_b = var_b --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() [Ref] L32: inner() -> var_b = var_b [Chg] L34: inner() -> var_b = CHANGED --- L17: After decorate --- L38: In outer() -> var_b = CHANGED --- Finish
var_c & var_d & var_e
All variables created in decorator()
; var_c
, var_d
and var_e
are defined in different scopes. But in the case of just referring from inner()
, the difference between these scopes doesn't matter. Let's only take var_c
as a representative.
First of all, var_c
cannot be called from outer()
because decorator()
has the different scope from the one of outer()
. We should comment out line 26 and 38 before executing the code.
## Error --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_c = var_c --- L10: In _decorator() -> var_c = var_c --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_c = var_c --- L31: Into inner() Traceback (most recent call last): File "c_deco.py", line 42, in <module> outer("var_f") File "c_deco.py", line 37, in outer inner("var_g") File "c_deco.py", line 16, in wrapper f(var_g) File "c_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_c = {}'.format(var_c)) UnboundLocalError: local variable 'var_c' referenced before assignment
We caught UnboundLocalError
. var_c
cannot be directly referred to from inner()
, so to comment out var_c = "CHANGED"
like the example of #1 would not work. In order to refer to var_c
from inner()
, we need to pass var_c
to the parameter of inner()
like #2. The result of changing to f(var_c)
and def inner(var_c)
at line 16 and 29 is the following.
# Result --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_c = var_c --- L10: In _decorator() -> var_c = var_c --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_c = var_c --- L31: Into inner() [Ref] L32: inner() -> var_c = var_c [Chg] L34: inner() -> var_c = var_c --- L17: After decorate --- Finish
Note that the function wrapped by the decorator cannot directly refer objects in the scope of the decorator itself. Functions in decorator()
, _dacorator()
and wrapper()
can directly refer to var_c
, but outer()
and inner()
which have different scopes cannot refer to it, and vice versa. We can see the word 'wrapper' when using decorators, but the word 'background' would be better to explain a decorator than it. The word 'wrapper' might somehow be misleading.
var_f
Next, we are going to refer the objects made of arguments, not variables.
var_f
is the arguments object of outer()
. It cannot be called from decorator()
, so we need to comment out line 6, 10 and 14 before execution.
# Error --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L26: In outer() -> var_f = var_f --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() Traceback (most recent call last): File "f_deco.py", line 42, in <module> outer("var_f") File "f_deco.py", line 37, in outer inner("var_g") File "f_deco.py", line 16, in wrapper f(var_g) File "f_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_f = {}'.format(var_f)) UnboundLocalError: local variable 'var_f' referenced before assignment
This case is almost the same as var_b
in outer()
. You can refer it as the ways of #1 or #2, or by using nonlocal keyword for var_b
. Note that nonlocal keyword is valid for objects made of arguments.
var_g
var_g
is the arguments object of inner()
. It is created when inner()
is called, and not able to be referred from outer()
and decorator()
. Comment out line 6, 10, 14, 26 and 38.
# Result --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L36: Execute inner() --- L13: Before decorate --- L31: Into inner() [Ref] L32: inner() -> var_g = var_g [Chg] L34: inner() -> var_g = CHANGED --- L17: After decorate --- Finish
There was not any error in the case of var_g
.
var_h
Finally, we are going to take var_h
which is the argument object of decorator()
. Comment out line 6, 10 and 14 before execution because they cannot be referred to from decorator()
.
# Error --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_h = var_h --- L10: In _decorator() -> var_h = var_h --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_h = var_h --- L31: Into inner() Traceback (most recent call last): File "h_deco.py", line 42, in <module> outer("var_f") File "h_deco.py", line 37, in outer inner("var_g") File "h_deco.py", line 16, in wrapper f(var_g) File "h_deco.py", line 32, in inner print('[Ref] L32: inner() -> var_h = {}'.format(var_h)) UnboundLocalError: local variable 'var_h' referenced before assignment
This case is also same as the result of var_c
. You can fix the error by passing var_h
to the argument of inner()
in wrapper()
.
# Result --- L3: Define decorator() --- L22: Define outer() --- L41: Execute outer() --- L24: Into outer() --- L6: In decorator() -> var_h = var_h --- L10: In _decorator() -> var_h = var_h --- L36: Execute inner() --- L13: Before decorate --- L14: In wrapper() -> var_h = var_h --- L31: Into inner() [Ref] L32: inner() -> var_h = var_h [Chg] L34: inner() -> var_h = CHANGED --- L17: After decorate --- Finish
Now, you might realize that the one of attributes of Python's decorator is that you can alter arguments of the function wrapped in the decorator. The decorator can handle the same arguments as the ones of the function wrapped by it. On the other hand, the objects used in wrapper()
cannot be referred to from other scopes unless the objects are passed to the parameter of wrapper()
.
Appendix: Examining a complex decorator
In addition to the above experiments, we will try to replace all variables and parameters to var_i
and update them at each scope. Are you wondering what value each var_i
has?
The code is the following. I added nonlocal var_i
to the line 8 because we cannot update the objects in decorator()
from _decorator()
directly.
print('--- L1: Define decorator()') def decorator(var_i): print('--- L3: In decorator() -> var_i = {}'.format(var_i)) var_i = "decorator" print('--- L5: In decorator() -> var_i = {}'.format(var_i)) def _decorator(f): nonlocal var_i print('--- L9: In _decorator() -> var_i = {}'.format(var_i)) var_i = "_decorator" print('--- L11: In _decorator() -> var_i = {}'.format(var_i)) def wrapper(var_i): print("--- L14: Before decorate") print('--- L15: In wrapper() -> var_i = {}'.format(var_i)) var_i = "wrapper" f(var_i) print("--- L18: After decorate") return wrapper return _decorator print('--- L23: Define outer()') def outer(): print("--- L25: Into outer()") var_i = "var_i" print('--- L27: In outer() -> var_i = {}'.format(var_i)) @decorator(var_i) def inner(var_i): # Examine which variables we can refer or update within the function "inner" print("--- L32: Into inner()") print('[Ref] L33: inner() -> var_i = {}'.format(var_i)) var_i = "CHANGED" print('[Chg] L35: inner() -> var_i = {}'.format(var_i)) print('--- L37: Execute inner()') inner(var_i) print('--- L39: In outer() -> var_i = {}'.format(var_i)) print('--- L42: Execute outer()') outer() print('--- Finish')
## Result --- L1: Define decorator() --- L23: Define outer() --- L42: Execute outer() --- L25: Into outer() --- L27: In outer() -> var_i = var_i --- L3: In decorator() -> var_i = var_i --- L5: In decorator() -> var_i = decorator --- L9: In _decorator() -> var_i = decorator --- L11: In _decorator() -> var_i = _decorator --- L37: Execute inner() --- L14: Before decorate --- L15: In wrapper() -> var_i = var_i --- L32: Into inner() [Ref] L33: inner() -> var_i = wrapper [Chg] L35: inner() -> var_i = CHANGED --- L18: After decorate --- L39: In outer() -> var_i = var_i --- Finish
After defining decorator()
and outer()
, var_i
has the value var_i
until it is replaced to the value decorator
at line 4. But after it exits decorator()
, the value of var_i
is restored to var_i
. When inner()
is executed, var_i
has the value wrapper
which is assigned in wrapper()
. What the complicated feature it is!
Okay, let's wrap up these examinations.
Summary
Be aware of scope relationships to define decorators effectively
If you would ignore scope relationships, you might not be able to implement decorators well. Make sure there are no dependencies between objects in a decorator and ones in a wrapped function. Also, keep in mind to make decorators general-purpose for various functions.
You can alter arguments of a wrapped function within a decorator
One of the powerful features of Python's decorator is brought out when performing a common process for arguments. The web application framework, Flask is a superb example of using decorators.
View Decorators — Flask Documentation (1.1.x)
Define a wrapped function AFTER executing outside of wrapper()
As you can see the examine of var_i
, the outside of wrapper()
is executed when the wrapped function is defined, while the inside of wrapper()
is executed when the wrapped function is called. You need to understand the difference between them and decide which process is supposed to be written inside or outside wrapper()
.
global
and nonlocal
does not mean altering local variables to global variables and nonlocal variables
It was just my misunderstanding, but global keyword and local keyword do not mean that they redefine local variables as global and nonlocal. The behavior of these keyword is tying a local variable to an outer variable with the same name. If a global variable with the same name as a local variable doesn't exist, you cannot use global keyword for it.
You can use nonlocal keyword for objects as arguments
This means a variable which has a complicated scope might exist.
Have a better Python life!